/* 
* Copyright (c) 2009, The University of Texas at Austin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, 
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, 
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, 
* this list of conditions and the following disclaimer in the documentation and/or other materials 
* provided with the distribution.
*
* Neither the name of The University of Texas at Austin nor the names of its contributors may be 
* used to endorse or promote products derived from this software without specific prior written 
* permission.
* 
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Bio.Data.Interfaces;

namespace Bio.Data.Providers.Alignment.FastA
{
    /// <summary>
    /// This class is used to load a set of RNA sequences from a FASTA file.
    /// </summary>
    public class FastAFile : BioFileDataLoader<IBioSequence>
    {
        private const int SmallFileSize = 5*1024*1024; // 5M
        internal MemoryMappedFile MmFile { get; private set; }

        /// <summary>
        /// This method is used to prepare the loader to access the
        /// Entities collection.
        /// </summary>
        protected override void OnLoad()
        {
            // Debug: special flag to load entire file into memory.  Place into 
            // <appSettings /> in the app.config file of controlling application.
            string value = ConfigurationManager.AppSettings["FastAFile:LoadIntoMemory"];
            bool loadIntoMemory = false;
            if (!string.IsNullOrEmpty(value))
                Boolean.TryParse(value, out loadIntoMemory);
            if (!loadIntoMemory)
                loadIntoMemory = (new FileInfo(InitializationData).Length < SmallFileSize);

            if (loadIntoMemory)
                LoadEntireFileIntoMemory();
            else
                MarkFileForVirtualization();
        }

        /// <summary>
        /// This parses the header out of the FastA header line.
        /// </summary>
        /// <param name="line"></param>
        /// <param name="commName"></param>
        /// <param name="sciName"></param>
        /// <param name="index"></param>
        private void ParseHeader(string line, out string commName, out string sciName, out string index)
        {
            sciName = commName = index = null;
            string[] bits = line.Substring(1).Split(new[] { "::" }, StringSplitOptions.None);
            if (bits.Length == 3)
            {
                index = bits[0].Trim();
                sciName = bits[1].Trim();
                commName = bits[2].Trim();
            }
            else
                sciName = line.Substring(1).Trim();

            if (string.IsNullOrEmpty(commName))
                commName = "<Not Available>";
            if (string.IsNullOrEmpty(sciName))
                sciName = commName;
            if (string.IsNullOrEmpty(index))
                index = (Entities.Count + 1).ToString();
        }

        /// <summary>
        /// Loads the FASTA file into memory
        /// </summary>
        private void LoadEntireFileIntoMemory()
        {
            // Load the file
            using (var ts = new StreamReader(InitializationData))
            {
                string line = ts.ReadLine();
                while (line != null)
                {
                    // Header for species?
                    if (line.StartsWith(">"))
                    {
                        // Parse out the header.
                        string commName, sciName, index;
                        ParseHeader(line, out commName, out sciName, out index);
                        
                        SimpleRNASequence sequence = new SimpleRNASequence(BioValidator.rRNAValidator) { Name = commName, ScientificName = sciName, Id = index };
                        Entities.Add(sequence);

                        // Load the data into memory.
                        for (; ; )
                        {
                            line = ts.ReadLine();
                            if (string.IsNullOrEmpty(line) || line.StartsWith(">"))
                                break;

                            // Add the symbols
                            foreach (var bs in line.Select(ch => FastAFile.CreateBioSymbol(sequence.Validator, ch)))
                                sequence.AddSymbol(bs);
                        }
                    }
                    // Skip unknown lines
                    else
                        line = ts.ReadLine();
                }
            }

            // Go through the entities and find the largest one.
            //   Then force all the sequences to have that count
            int maxSize = Entities.Max(seq => seq.SequenceData.Count);
            Parallel.ForEach(Entities, (e) =>
            {
                int addCount = maxSize - e.SequenceData.Count;
                for (int i = 0; i < addCount; i++)
                    e.SequenceData.Add(BioSymbol.None);
            });
        }

        /// <summary>
        /// This reads through the file and identifies all headers and positions.
        /// </summary>
        private void MarkFileForVirtualization()
        {
            // Open the file using a memory mapped reader.
            MmFile = MemoryMappedFile.CreateFromFile(InitializationData, FileMode.Open);
            using (var fs = MmFile.CreateViewStream(0, 0, MemoryMappedFileAccess.Read))
            {
                for (; ; )
                {
                    int b = fs.ReadByte();
                    if (b == 0x0 || b == -1) // EOF
                        break;

                    if ((char)b == '>')
                    {
                        // Read to the end of the line.
                        fs.Seek(-1, SeekOrigin.Current);
                        string line = fs.ReadLine();
                        if (line == null)
                            break;

                        // Parse out the header.
                        string commName, sciName, index;
                        IBioValidator validator = BioValidator.rRNAValidator;
                        ParseHeader(line, out commName, out sciName, out index);

                        // Index through the file - mark each row for each sequence so we can
                        // quickly jump to a known nucelotide position.
                        List<Tuple<int,long>> positions = new List<Tuple<int, long>>();
                        positions.Add(new Tuple<int, long>(0, fs.Position));

                        // Go through the file - looking for each line and header.
                        long startPos = fs.Position;
                        int count = 0, startingNonGap = -1;
                        do
                        {
                            b = fs.ReadByte();
                            if (b <= 0)
                                break;
                            else if (b == 0xa) // EOL
                            {
                                positions.Add(new Tuple<int, long>(count + 1, fs.Position + 1));
                            }
                            else if ((char) b != '>' && b != 0xd && b != 0x0a)
                            {
                                count++;
                                if (startingNonGap == -1 && validator.IsValid((char) b))
                                    startingNonGap = (int) (fs.Position - startPos);
                            }
                        } while ((char) b != '>');

                        long endPos = fs.Position;
                        fs.Seek(-1, SeekOrigin.Current);

                        if (startingNonGap == -1)
                            startingNonGap = count;

                        // Add the entity
                        Entities.Add(new VirtualizedFastASequence(this, BioValidator.rRNAValidator, startingNonGap, positions, count, (int)(endPos - startPos) - 1)
                         {
                             Name = commName, ScientificName = sciName, Id = index
                         });
                    }
                }
            }

            // Go through the entities and find the largest one.
            //   Then force all the sequences to have that count
            int maxSize = Entities.Max(seq => seq.SequenceData.Count);
            foreach (VirtualizedFastASequence e in Entities)
                e.ForceCount(maxSize);
        }

        /// <summary>
        /// Disposal method to dump the sequences and memory mapped file.
        /// </summary>
        /// <param name="isDisposing"></param>
        protected override void Dispose(bool isDisposing)
        {
            if (isDisposing) 
            {
                if (MmFile != null)
                {
                    MmFile.Dispose();
                    MmFile = null;
                }
            }
        }

        /// <summary>
        /// Creates a BioSymbol from a character in a FASTA file
        /// </summary>
        /// <param name="validator"></param>
        /// <param name="ch"></param>
        /// <returns></returns>
        internal static IBioSymbol CreateBioSymbol(IBioValidator validator, char ch)
        {
            return (ch == '-') ? BioSymbol.Gap : (ch == '~') ? BioSymbol.None
                     : validator.IsValid(ch)
                           ? new BioSymbol(BioSymbolType.Nucleotide, ch)
                           : new BioSymbol(BioSymbolType.Unknown, ch);
        }
    }
}